/***************************************************************
*  Copyright (c) 2007 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.controller.oauth;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

import net.groupme.model.ObjectFactory;
import net.groupme.utils.sql.SelectQueryUtility;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
import net.oauth.OAuthValidator;
import net.oauth.SimpleOAuthValidator;
import net.oauth.server.OAuthServlet;

/** 
 * This class that provides the core GroupMe!-specific OAuth functionality.
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: fabian $
 * 
 * @version created on Feb 26, 2009
 * @version $Revision: 1.3 $ $Date: 2010-09-29 19:04:43 $
 */
public class OAuthProvider {
	
	/** Logger for this class and subclasses */
	protected static final Log logger = LogFactory.getLog(OAuthProvider.class);

	public static final OAuthValidator VALIDATOR = new SimpleOAuthValidator();

	 /** stores the list of all consumers */
	 private static final Map<String, OAuthConsumer> ALL_CONSUMERS 
	                    = Collections.synchronizedMap(new HashMap<String,OAuthConsumer>(10));
	    
	 private static final Collection<OAuthAccessor> ALL_TOKENS = new HashSet<OAuthAccessor>();

	 private static Properties consumerProperties = null;

	 static{
		 try {
			loadConsumers();
		} catch (IOException e) {
			e.printStackTrace();
		}
	 }
	 @SuppressWarnings("unchecked")
	 public static synchronized void loadConsumers() throws IOException {
     Properties p = consumerProperties;
      if (p == null) {
          p = new Properties();
          String resourceName = "/"
                  + OAuthProvider.class.getPackage().getName().replace(
                  ".", "/") + "/provider.properties";
          URL resource = OAuthProvider.class.getClassLoader()
          .getResource(resourceName);
          if (resource == null) {
              throw new IOException("resource not found: " + resourceName);
          }
          InputStream stream = resource.openStream();
          try {
              p.load(stream);
          } finally {
              stream.close();
          }
      }
      consumerProperties = p;
      
      // for each entry in the properties file create a OAuthConsumer
      for(Map.Entry prop : p.entrySet()) {
          String consumer_key = (String) prop.getKey();
          // make sure it's key not additional properties
          if(!consumer_key.contains(".")){
              String consumer_secret = (String) prop.getValue();
              if(consumer_secret != null){
                  String consumer_description = (String) p.getProperty(consumer_key + ".description");
                  String consumer_callback_url =  (String) p.getProperty(consumer_key + ".callbackURL");
                  // Create OAuthConsumer w/ key and secret
                  OAuthConsumer consumer = new OAuthConsumer(
                          consumer_callback_url, 
                          consumer_key, 
                          consumer_secret, 
                          null);
                  consumer.setProperty("name", consumer_key);
                  consumer.setProperty("description", consumer_description);
                  ALL_CONSUMERS.put(consumer_key, consumer);
              }
          }
      }
      initAllTokens();
  }
	 
	 private static synchronized void initAllTokens(){
		 ResultSet result = SelectQueryUtility.executeQuery("SELECT consumerKey, accessToken, tokenSecret, userId, authorized FROM OAuth");
		 try {
			while(result.next()){
				if(ALL_CONSUMERS.get(result.getString(1)) != null){
					 OAuthAccessor accessor = new OAuthAccessor(ALL_CONSUMERS.get(result.getString(1)));
					 accessor.accessToken = result.getString(2);
					 accessor.tokenSecret = result.getString(3);
					 accessor.setProperty("user", result.getString(4));
					 if(!"no".equals(result.getString(5))){
						 accessor.setProperty("authorized", Boolean.TRUE);
					 }else{
						 accessor.setProperty("authorized", Boolean.FALSE);
					 }
				    ALL_TOKENS.add(accessor);
				}
			 }
		} catch (SQLException e) {
			try{
				//create table and try a second time:
				createOAuthTable();
				initAllTokens();
			}catch(Exception e2){
				//do nothing
			}
		}
	 }
	 private static synchronized void createOAuthTable(){
		 String newTable = "CREATE TABLE OAuth (id int(11) NOT NULL auto_increment," +
		 		" consumerKey varchar(1500) default NULL, " +
		 		" accessToken varchar(1500) default NULL,  " +
		 		" tokenSecret varchar(1500) default NULL, " +
		 		" userId int(11) NOT NULL, " +
		 		" authorized set('yes','no') NOT NULL default 'yes', " +
		 		" timeAndDate timestamp NOT NULL default CURRENT_TIMESTAMP, " +
		 		" PRIMARY KEY  (id)) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;" ;
		SqlUpdate su = new SqlUpdate(ObjectFactory.ds, newTable);
		su.update();
	 }

  /**
	 * This method checks if the consumer application is already known to the GroupMe! system.
	 * If it is not known then an {@link OAuthProblemException} is thrown.
	 * @param requestMessage the request message should contain the key of the consumer.
	 * @return the {@link OAuthConsumer} instance that corresponds to the given consumer key.
	 * @throws IOException
	 * @throws OAuthProblemException if the consumer key is not known 
	 */
	public static synchronized OAuthConsumer getConsumer(
          OAuthMessage requestMessage)
          throws IOException, OAuthProblemException {
      
      OAuthConsumer consumer = null;
      // try to load from local cache if not throw exception
      String consumer_key = requestMessage.getConsumerKey();
      
      consumer = ALL_CONSUMERS.get(consumer_key);
      
      if(consumer == null) {
          OAuthProblemException problem = new OAuthProblemException("token_rejected");
          throw problem;
      }
      
      return consumer;
  }
	
  
  /**
   * Get the access token and token secret for the given oauth_token. 
   */
  public static synchronized OAuthAccessor getAccessor(OAuthMessage requestMessage)
          throws IOException, OAuthProblemException {
      
      // try to load from local cache if not throw exception
      String consumer_token = requestMessage.getToken();
      OAuthAccessor accessor = null;
      for (OAuthAccessor a : ALL_TOKENS) {
          if(a.requestToken != null) {
              if (a.requestToken.equals(consumer_token)) {
                  accessor = a;
                  break;
              }
          } else if(a.accessToken != null){
              if (a.accessToken.equals(consumer_token)) {
                  accessor = a;
                  break;
              }
          }
      }
      
      // try to get accessor from DB:
      if(accessor == null){
	      try{
	    	  logger.info("OAuthAccessor is null. Try to get accessor from DB.");
	    	  ResultSet result = SelectQueryUtility.executeQuery("SELECT consumerKey, accessToken, tokenSecret, userId, authorized FROM OAuth" +
	    	  		" where accessToken = \"" + consumer_token + "\" OR tokenSecret = \"" + consumer_token + "\"");
	    	  System.out.println();
	    	  if(result.next()){
	    		  accessor = new OAuthAccessor(ALL_CONSUMERS.get(result.getString(1)));
	    		  accessor.accessToken = result.getString(2);
	    		  accessor.tokenSecret = result.getString(3);
	    		  accessor.setProperty("user", result.getString(4));
	    		  if(!"no".equals(result.getString(5))){
	    			  accessor.setProperty("authorized", Boolean.TRUE);
	    		  }else{
	    			  accessor.setProperty("authorized", Boolean.FALSE);
	    		  }
				  ALL_TOKENS.add(accessor);
				  logger.info("Found accessor in DB. Nice!");
	    	  }
	      }catch (Exception e) {
	    	  logger.info("Obtaining OAuthAccessor from DB failed.");
	      }
      }
      
      
      if(accessor == null){
          OAuthProblemException problem = new OAuthProblemException("token_expired");
          throw problem;
      }
      
      return accessor;
  }

  /**
   * Set the access token 
   */
  public static synchronized void markAsAuthorized(OAuthAccessor accessor, String userId)
          throws OAuthException {
      
      
      // first remove the accessor from cache
      ALL_TOKENS.remove(accessor);
      
      accessor.setProperty("user", userId);   
      accessor.setProperty("authorized", Boolean.TRUE);
      
      // update token in local cache
      ALL_TOKENS.add(accessor);
  }
  
//  public static 
  
  private static void updateOAuthDBTable(String consumerKey, String accessToken, String tokenSecret, String userId, Boolean authorized){
	  boolean update = false;
	   ResultSet result = SelectQueryUtility.executeQuery("SELECT consumerKey, userId FROM OAuth WHERE consumerKey = '" + consumerKey + "' AND userId = " + userId);
	   try {
			while(result.next()){
				update = true;
			}
		} catch (SQLException e) {
			//do nothing, just insert
		}
		SqlUpdate su = null;
		if(update){
			su = new SqlUpdate(
					ObjectFactory.ds,
					"Update OAuth SET  accessToken = '" + accessToken +"', tokenSecret = '" + tokenSecret +"', authorized ='" + (authorized ? "yes" : "no") + "' WHERE userId = " + userId + " AND consumerKey = '" + consumerKey + "'"); //$NON-NLS-1$
			su.compile();
			su.update();
		}else{
		   su = new SqlUpdate(
					ObjectFactory.ds,
					"Insert into OAuth (consumerKey, accessToken, tokenSecret, userId, authorized) VALUES (?,?,?,?,?)"); //$NON-NLS-1$ 
			su.declareParameter(new SqlParameter("consumerKey", Types.VARCHAR)); //$NON-NLS-1$
			su.declareParameter(new SqlParameter("accessToken", Types.VARCHAR)); //$NON-NLS-1$
			su.declareParameter(new SqlParameter("tokenSecret", Types.VARCHAR)); //$NON-NLS-1$
			su.declareParameter(new SqlParameter("userId", Types.INTEGER)); //$NON-NLS-1$
			su.declareParameter(new SqlParameter("authorized", Types.VARCHAR)); //$NON-NLS-1$
			su.compile();
			Object[] values = {
					consumerKey,
					accessToken,
					tokenSecret,
					new Integer(userId),
					(authorized ? "yes" : "no")
			};
			su.update(values);
		}
  }

  /**
   * Generate a fresh request token and secret for a consumer.
   * 
   * @throws OAuthException
   */
  public static synchronized void generateRequestToken(
          OAuthAccessor accessor)
          throws OAuthException {

      // generate oauth_token and oauth_secret
      String consumer_key = (String) accessor.consumer.getProperty("name");
      // generate token and secret based on consumer_key
      
      // for now use md5 of name + current time as token
      String token_data = consumer_key + System.nanoTime();
      String token = DigestUtils.md5Hex(token_data);
      // for now use md5 of name + current time + token as secret
      String secret_data = consumer_key + System.nanoTime() + token;
      String secret = DigestUtils.md5Hex(secret_data);
      
      accessor.requestToken = token;
      accessor.tokenSecret = secret;
      accessor.accessToken = null;
      
      // add to the local cache
      ALL_TOKENS.add(accessor);
      
  }
  
  /**
   * Generate a fresh request token and secret for a consumer.
   * 
   * @throws OAuthException
   */
  public static synchronized void generateAccessToken(OAuthAccessor accessor)
          throws OAuthException {

      // generate oauth_token and oauth_secret
      String consumer_key = (String) accessor.consumer.getProperty("name");
      // generate token and secret based on consumer_key
      
      // for now use md5 of name + current time as token
      String token_data = consumer_key + System.nanoTime();
      String token = DigestUtils.md5Hex(token_data);
      // first remove the accessor from cache
      ALL_TOKENS.remove(accessor);
      
      accessor.requestToken = null;
      accessor.accessToken = token;
      
      // update token in local cache
      ALL_TOKENS.add(accessor);
      
      //update access token in DB:
      if(accessor.accessToken != null){
    	  updateOAuthDBTable(accessor.consumer.consumerKey, accessor.accessToken, accessor.tokenSecret, accessor.getProperty("user").toString(), true);
      }
  }
  
  public static void handleException(Exception e, HttpServletRequest request,
          HttpServletResponse response, boolean sendBody)
          throws IOException, ServletException {
      String realm = (request.isSecure())?"https://":"http://";
      realm += request.getLocalName();
      OAuthServlet.handleException(response, e, realm, sendBody); 
  }

  public static OAuthMessage prepare(OAuthMessage requestMessage) {
	  if(requestMessage.URL != null && requestMessage.URL.contains("out.l3s.uni-hannover.de:8080")){
		  requestMessage.URL = requestMessage.URL.replace("out.l3s.uni-hannover.de:8080", "groupme.org");
      }
	  return requestMessage;
  }
  
    
//    public static void testPrintStuff(){
//    	String result = "### Tokens:\n";
//    	for(OAuthAccessor accessor: ALL_TOKENS){
//    		result += accessor.consumer.consumerKey + ": (accessToken, " + accessor.accessToken + "), (requestToken, " + accessor.requestToken + "), (tokenSecret, " + accessor.tokenSecret + "), (user, " + accessor.getProperty("user") + "), (authorized, " + accessor.getProperty("authorized") + ") \n";
//    		
//    	}
//    	
//    	result +="\n";
//    	for(String key: ALL_CONSUMERS.keySet()){
//    		OAuthConsumer consumer = ALL_CONSUMERS.get(key);
//    		result += key + ": " + consumer.consumerKey + " [" + consumer.getProperty(OAuthConsumer.ACCESSOR_SECRET) + "]";
//    	}
//    	System.out.println(result + "\n#####");
//    }
}
